/*
 * Created on Jul 11, 2004
 */
package dk.aiszone.lang.autosource.generatorobjects;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import dk.aiszone.lang.autosource.generatorobjects.helpers.ImportsList;
import dk.aiszone.lang.autosource.generatorobjects.interfaces.DeclarableObject;
import dk.aiszone.lang.autosource.generatorobjects.interfaces.GeneratorObject;
import dk.aiszone.lang.autosource.generatorobjects.interfaces.Parent;

/**
 * The <code>AbstractBody</code> class contains the common parts between the <code>ClassBody</code> and the <code>InterfaceBody</code> classes.<br>
 * The <code>AbstractBody</code> either allows <code>Methods</code> or <code>MethodDeclarations</code> to be added.
 * 
 * @see ClassBody
 * @see InterfaceBody
 * 
 * @param <T>
 *            Determines the type of contents of the <code>AbstractBody</code>.
 * 
 * @author <a href="mailto:ais@mail.dk">Ais</a>
 * @since 1.0
 * @version 1.0
 */
public abstract class AbstractBody<T extends MethodDeclaration> extends GeneratorObject implements Cloneable, Comparable<AbstractBody<? extends MethodDeclaration>>, DeclarableObject, Parent {

    /** <code>List</code> of <code>Annotation</code> objects. */
    protected final List<Annotation> annotations;

    /** The <code>Declaration</code> of the <code>AbstractBody</code> */
    protected Declaration declaration;

    /** <code>List</code> of extended classes (<code>List&lt;Class&gt;</code>) */
    protected final List<Class> extendsClasses;

    /** Generics list (<code>List&lt;GenericClass&gt;</code>) */
    protected final List<GenericClass> generics;

    /** <code>List</code> of inner classes within the <code>ClassBody</code> (<code>List&lt;AbstractBody&gt;</code>) */
    protected List<AbstractBody<? extends MethodDeclaration>> innerClasses;

    /** The needed imports of the <code>AbstractBody</code> */
    protected final ImportsList importsUsedByBody;

    /** The <code>Methods</code> or <code>MethodDeclarations</code> of the <code>AbstractBody</code> (<code>Map&lt;String, Set&lt;T&gt;&gt;</code>) */
    protected final Map<String, Set<T>> methods;

    /** The name of the <code>AbstractBody</code> */
    protected String name;

    private JavaDoc javadoc;

    private Parent parent;

    /**
     * Default constructor of the <code>AbstractBody</code> class.<br>
     * Calling this constructor requires the <code>name</code> and <code>declaration</code> to be set later on.<br>
     * Failing to do so can cause validation problems when transforming the body to text.
     * 
     * @see #setName(String)
     * @see #setDeclaration(Declaration)
     */
    protected AbstractBody() {
        this.methods = new TreeMap<String, Set<T>>();

        this.importsUsedByBody = new ImportsList();
        this.annotations = new ArrayList<Annotation>();
        this.generics = new ArrayList<GenericClass>();
        this.extendsClasses = new ArrayList<Class>();
        this.innerClasses = new ArrayList<AbstractBody<? extends MethodDeclaration>>();
    }

    /**
     * Constructor for the <code>AbstractBody</code> class.<br>
     * 
     * @param declaration
     *            The <code>Declaration</code> of the body.
     * @param name
     *            The name of the body.
     */
    protected AbstractBody(Declaration declaration, String name) {
        this();
        this.declaration = declaration;
        this.name = name;
    }

    /**
     * Add an <code>Annotation</code> to the <code>AbstractBody</code>
     * 
     * @param annotation
     *            The <code>Annotation</code> to add.
     */
    public void addAnnotation(Annotation annotation) {
        importsUsedByBody.addAll(annotation.getNeededImports());
        annotations.add(annotation);
    }

    /**
     * Add the generic declaration to the body.<br>
     * Generics will normally we appended to the body header:<br>
     * 
     * <pre>
     *      public class TestClass&lt;b&gt;&lt;T extends Object&gt;&lt;/b&gt;
     *      
     *      public class TestClass&lt;b&gt;&lt;Object&gt;&lt;/b&gt;
     * </pre>
     * 
     * @param generics
     */
    public void addGenerics(GenericClass generics) {
        this.generics.add(generics);
    }

    /**
     * Add an inner class to the <code>AbstractBody</code>.
     * 
     * @param innerClass
     *            <code>AbstractBody</code> to be added as inner class.
     */
    public void addInnerclass(AbstractBody<? extends MethodDeclaration> innerClass) {
        innerClasses.add(innerClass);
    }

    /**
     * Add a <code>Method</code> or <code>MethodDeclaration</code> to the body.
     * 
     * @param method
     *            <code>Method</code> or <code>MethodDeclaration</code> to be added.<br>
     *            Multiple <code>Methods</code> or <code>MethodDeclarations</code> with the same name can be added to the body.<br>
     *            But adding the same <code>Method</code> or <code>MethodDeclaration</code> twice will cause the second instance to be ignored.
     */
    public void addMethod(T method) {
        if (!this.methods.containsKey(method.getName())) {
            this.methods.put(method.getName(), new HashSet<T>());
        }

        this.methods.get(method.getName()).add(method);
    }

    /**
     * Adds an <code>Import</code> to the body.
     * 
     * @param _import
     *            <code>Import</code> needed by the body.
     */
    public void addNeededImport(Import _import) {
        importsUsedByBody.addImport(_import);
    }

    /**
     * Add the supplied <code>Imports</code> to the <code>Imports</code> needed by the <code>AbstractBody</code>.
     * 
     * @param _imports
     *            <code>Set</code> of needed <code>Imports</code> (<code>Set&lt;Import&gt;</code>)
     */
    public void addNeededImports(Set<Import> _imports) {
        importsUsedByBody.addImports(_imports);
    }

    public int compareTo(AbstractBody<? extends MethodDeclaration> other) {
        return name.compareTo(other.name);
    }

    /**
     * Retrieve a <code>List</code> of <code>Annotations</code> declared on the <code>AbstractBody</code>.
     * 
     * @return <code>List</code> of <code>Annotations</code>.
     */
    public List<Annotation> getAnnotations() {
        return annotations;
    }

    /**
     * Retrieve the <code>Declaration</code> of the body
     * 
     * @return The body <code>Declaration</code> or <code>null</code> if unset.
     */
    public Declaration getDeclaration() {
        return declaration;
    }

    /**
     * Retrieve a <code>List</code> of <code>Classes</code> representing the generics declaration of the body.
     * 
     * @return <code>List</code> of <code>Classes</code> (<code>List&lt;GenericClass&gt;</code>).
     */
    public List<GenericClass> getGenerics() {
        return generics;
    }

    /**
     * Retrieve the inner classes contained by the <code>AbstractBody</code>
     * 
     * @return Unmodifiable <code>List</code> of inner clases.
     */
    public List<AbstractBody<? extends MethodDeclaration>> getInnerClasses() {
        return Collections.unmodifiableList(innerClasses);
    }

    /**
     * Retrieve the <code>JavaDoc</code> of the <code>AbstractBody</code>
     * 
     * @return The declared <code>JavaDoc</code> object.
     */
    public JavaDoc getJavaDoc() {
        if (javadoc == null) {
            setJavaDoc(new JavaDoc());
        }
        return javadoc;
    }

    /**
     * Retrieve a <code>List</code> of <code>Methods</code> or <code>MethodDeclarations</code> associated with the specified name.
     * 
     * @param name
     *            The name of the <code>Methods</code> or <code>MethodDeclarations</code> to retrieve.
     * @return <code>List</code> of <code>Method</code> or <code>MethodDeclarations</code> (<code>List&lt;Method&gt;</code> /
     *         <code>List&lt;MethodDeclaration&gt;</code>)<br>
     *         or an empty <code>List</code> if no <code>Methods</code> or <code>MethodDeclarations</code> with supplied name was found.
     */
    public List<T> getMetod(String name) {
        Set<T> value = methods.get(name);
        return new ArrayList<T>(value);
    }

    /**
     * Retrieve the name of the body.
     * 
     * @return The body name or <code>null</code> if unset.
     */
    public String getName() {
        return name;
    }

    /**
     * Retrieve an <code>ImportList</code> containing the <code>Imports</code> needed to <code>this</code> body.<br>
     * The returned <code>ImportList</code> will contain all the <code>Imports</code> needed by:
     * <ul>
     * <li>The body itself (added with {@link #addNeededImport(Import)}
     * <li>The <code>Methods</code> or <code>MethodDeclarations</code> added to the body
     * </ul>
     * 
     * @return <code>ImportsList</code> containing the needed <code>Imports</code> of the this body.
     */
    public ImportsList getNeededImports() {
        for (Iterator<Set<T>> iter = methods.values().iterator(); iter.hasNext();) {
            addNeededImportsFromMethods(iter.next());
        }

        if (!extendsClasses.isEmpty()) {
            for (Iterator<Class> iter = extendsClasses.iterator(); iter.hasNext();) {
                Class _class = iter.next();

                if (_class.requiresImport()) {
                    addNeededImports(_class.toImports());
                }
            }
        }

        for (Iterator<GenericClass> iter = generics.iterator(); iter.hasNext();) {
            addNeededImports(iter.next().toImports());
        }

        return importsUsedByBody;
    }

    /**
     * Retrieve the package of the body.<br>
     * If the body has a parent, the complete package of the parent and the body is returned.<br>
     * If no parent is set - the name of the body is returned.
     * 
     * @return The package of the body.
     */
    public String getPackage() {
        if (parent != null) {
            return parent.getPackage() + "." + name;
        }
        return name;
    }

    /**
     * Set or update the <code>Declaration</code> of the body.
     * 
     * @param declaration
     *            The new <code>Declaration</code> of the body.
     */
    public void setDeclaration(Declaration declaration) {
        this.declaration = declaration;
    }

    /**
     * Set or update the <code>JavaDoc</code> of the body.
     * 
     * @param javadoc
     *            The <code>JavaDoc</code> of the body.
     */
    public void setJavaDoc(JavaDoc javadoc) {
        this.javadoc = javadoc;
    }

    /**
     * Set or update the name of the body.
     * 
     * @param name
     *            The new name of the body.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Converts the body to a <code>Class</code> declaration.
     * 
     * @return The <code>Class</code> representation of the body.
     */
    public Class toClass() {
        return new Class(getPackage());
    }

    /**
     * Appends the needed <code>Imports</code> from the supplied <code>Methods</code> or <code>MethodDeclarations</code> to the<br>
     * <code>ImportList</code> of the body.
     * 
     * @param set
     *            <code>Set</code> of <code>Methods</code> or <code>MethodDeclarations</code> to add.
     */
    protected void addNeededImportsFromMethods(Set<T> set) {
        for (Iterator<T> iter = set.iterator(); iter.hasNext();) {
            importsUsedByBody.addAll(iter.next().getNeededImports());
        }
    }

    /**
     * Deep clones every attribute on the <code>AbstractBody</code> objects with the exception of the parent attribute.<br>
     * This attribute is not cloned to the new object.
     * 
     * @param clone
     *            <code>AbstractBody</code> object to be popultated with the cloned values of this object
     * @throws CloneNotSupportedException
     */
    @SuppressWarnings("unchecked")
    protected void cloneDefaultAttributes(AbstractBody<T> clone) throws CloneNotSupportedException {
        if (declaration != null) {
            clone.declaration = (Declaration)declaration.clone();
        }

        // Jesting alters these lines, so the part inside the if-clause always executes.
        // This doesn't cause any test to fails - but is marginally "slower"
        // stopJesting
        if (!extendsClasses.isEmpty()) {
            clone.extendsClasses.addAll(extendsClasses);
        }
        // resumeJesting

        clone.generics.addAll(generics);
        clone.importsUsedByBody.addAll(importsUsedByBody);

        for (Iterator<Set<T>> iter = methods.values().iterator(); iter.hasNext();) {
            Set<T> set = iter.next();
            for (Iterator<T> iterator = set.iterator(); iterator.hasNext();) {
                clone.addMethod((T)iterator.next().clone());
            }
        }

        clone.name = name;
        clone.javadoc = javadoc;

        for (Iterator<? extends AbstractBody> iter = innerClasses.iterator(); iter.hasNext();) {
            clone.addInnerclass((AbstractBody)iter.next().clone());
        }
    }

    /**
     * Set the <code>Parent</code> of the <code>AbstractBody</code>
     * 
     * @param parent
     *            The <code>Parent</code> of the body.
     */
    /* pp */void setParent(Parent parent) {
        this.parent = parent;
    }
}